feature: dynamic (catch-all) workflows - #784
Conversation
Allow a PHP worker to register a dynamic (catch-all) workflow, invoked for any workflow type name that has no statically registered handler. The host already registers a single shared WorkflowDefinitionFactory proxy (wDef) under each declared name; when a workflow is declared dynamic, register that same proxy via worker.RegisterDynamicWorkflow instead (it forwards the real workflow type name to PHP), and skip named registration for it. WorkflowInfo gains a Dynamic field, populated from the "dynamic" key the PHP GetWorkerInfo handshake emits. This enables Dynamic Workflow support in the PHP SDK (PHP and TypeScript are the only SDKs without it). Companion change: temporalio/sdk-php. Depends on temporalio/sdk-go#2449 (issue #2448): RegisterDynamicWorkflow accepts the factory, but go-sdk's dynamic execution path panics on it until that fix ships. A go.temporal.io/sdk bump to the first release containing it is required before dynamic dispatch runs.
b6563aa to
d24901b
Compare
Add `#[WorkflowMethod(dynamic: true)]` to declare a dynamic (catch-all) workflow — invoked when the worker receives a workflow whose type name is not statically registered. WorkflowReader flags the prototype; StartWorkflow falls back to the dynamic prototype when no named workflow matches; and the GetWorkerInfo handshake advertises `dynamic` so the RoadRunner temporal plugin can register a Go dynamic-workflow proxy for it. As in the other SDKs (Go panics, Python raises), at most one dynamic workflow may be registered per worker — WorkflowCollection enforces this. Enables Dynamic Workflow support in the PHP SDK (PHP and TypeScript are the only SDKs without it). End-to-end dispatch also requires temporalio/roadrunner-temporal#784 (register the dynamic proxy) and temporalio/sdk-go#2449 (execute a factory-registered dynamic workflow).
|
Hey @datashaman 👋🏻 |
There was a problem hiding this comment.
Pull request overview
Adds support for PHP workers to register a dynamic (catch-all) workflow in the Go host, enabling a single PHP handler to receive otherwise-unregistered workflow type names while preserving the real type name for UI/identity.
Changes:
- Bump
go.temporal.io/sdktov1.47.0(root andtestsmodules) to pick up dynamic-workflow factory support. - Extend worker handshake model with
WorkflowInfo.Dynamicto mark workflows as catch-all. - Register dynamic workflows via
worker.RegisterDynamicWorkflow(and skip named registration) whenDynamicis set.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
go.mod |
Pins Temporal Go SDK to v1.47.0 in the main module. |
go.sum |
Adds checksums for go.temporal.io/sdk v1.47.0. |
tests/go.mod |
Pins Temporal Go SDK to v1.47.0 in the tests module. |
tests/go.sum |
Adds checksums for go.temporal.io/sdk v1.47.0 in the tests module. |
internal/worker_info.go |
Adds WorkflowInfo.Dynamic to carry the PHP handshake flag. |
aggregatedpool/workers.go |
Registers catch-all workflows using RegisterDynamicWorkflow when Dynamic is set. |
Comments suppressed due to low confidence (1)
aggregatedpool/workers.go:160
- The new dynamic-workflow registration path is not covered by automated tests. This file already has unit tests (e.g., for interceptor/converter resolution and registration panic handling), so it would be good to add a unit/integration test that exercises
wf.Dynamic == trueand asserts only dynamic registration occurs (and that the named registration is skipped).
if wf.Dynamic {
err := registerWorkflow(func() {
wrk.RegisterDynamicWorkflow(wDef, workflow.DynamicRegisterOptions{})
}, wf.Name, wi[i].TaskQueue)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add `#[WorkflowMethod(dynamic: true)]` to declare a dynamic (catch-all) workflow — invoked when the worker receives a workflow whose type name is not statically registered. WorkflowReader flags the prototype; StartWorkflow falls back to the dynamic prototype when no named workflow matches; and the GetWorkerInfo handshake advertises `dynamic` so the RoadRunner temporal plugin can register a Go dynamic-workflow proxy for it. As in the other SDKs (Go panics, Python raises), at most one dynamic workflow may be registered per worker — WorkflowCollection enforces this. Enables Dynamic Workflow support in the PHP SDK (PHP and TypeScript are the only SDKs without it). End-to-end dispatch also requires temporalio/roadrunner-temporal#784 (register the dynamic proxy) and temporalio/sdk-go#2449 (execute a factory-registered dynamic workflow).
|
Thanks @rustatian — agreed, this needs end-to-end coverage. The integration fixture currently installs released My proposed order is to merge/package sdk-php#774 first, then update the fixture here and add a test that starts an unregistered workflow type and verifies the PHP dynamic handler receives the real type name. Would you prefer that, or a temporary feature-branch pin in this PR? |
Thank you. I see that the SDK-PHP changes are more significant. So we can merge RR's part first, since anyway this is a chicken-and-egg problem. You can then write more tests on the SDK-PHP side. |
…flow # Conflicts: # .github/workflows/codeql-analysis.yml
|
Resolved the conflict with the latest I also aligned the Go version files on 1.26.5 (following #790 pattern), there was an existing mismatch on master. Unit tests, |
What
Lets a PHP worker register a dynamic (catch-all) workflow — invoked for any workflow type name that has no statically-registered handler. The host already registers a single shared
WorkflowDefinitionFactoryproxy (wDef) under each declared name; this adds the ability to register that same proxy viaworker.RegisterDynamicWorkflowwhen the PHP worker declares a workflow as dynamic.internal/worker_info.go— newWorkflowInfo.Dynamicfield, populated from thedynamickey the PHPGetWorkerInfohandshake emits.aggregatedpool/workers.go— when a declared workflow isDynamic, register it viaRegisterDynamicWorkflow(reusingwDef, which forwards the real workflow type name to PHP) and skip named registration for it.aggregatedpool/workers.go— reject multiple dynamic workflows on one task queue before registration, since more than one catch-all would be ambiguous.Why
Enables Dynamic Workflow support in the PHP SDK — PHP and TypeScript are the only SDKs without it. It lets applications that author workflows at runtime (e.g. UI-driven pipeline/automation builders) give each workflow its own type name — real identity in the Web UI — while a single PHP handler interprets it, with no codegen or per-workflow deploy. Companion PHP change: temporalio/sdk-php#774, which declares the dynamic workflow and emits the
dynamicWorkerInfofield.Dependency — satisfied
temporalio/sdk-go#2449 (issue temporalio/sdk-go#2448) fixed factory-backed dynamic workflow execution and shipped in
go.temporal.io/sdk v1.47.0. This PR now pins that release in both Go modules.Testing
go test ./...andgo vet ./...clean in both the root andtestsGo modules.TestTemporalWorkers_MultipleDynamicWorkflows_ReturnsErrorverifies that ambiguous dynamic workflow configuration is rejected before registration.rrbuilt from this change + Go SDK v1.47.0 + sdk-php#774: starting an unregistered type (pipeline-blog-publish) is caught by the shared proxy, dispatched to the PHP dynamic handler (which reads the real type viaWorkflow::getInfo()), and completes →{"dispatchedType":"pipeline-blog-publish"}. Without Go SDK v1.47.0, the same run fails with aWorkflowTaskFailedcarrying the reflect panic.Backwards compatibility
Additive.
Dynamicdefaults tofalse(omitted from the handshake JSON), so existing workers register exactly as before.